﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;

namespace NWebSockets
{
	public class WebSocketClient : IDisposable
	{
		/// <summary>
		/// Default constructor
		/// </summary>
		public WebSocketClient()
		{
			// initialize the buffer
			_buffer = new byte[_maxPacketSize];
		}

		/// <summary>
		/// Constructs a new WebSocket client
		/// </summary>
		public WebSocketClient( string hostName, int port, bool useSsl ) : this()
		{
			// store our connection settings
			this.HostName = hostName;
			this.Port = port;
			this.UseSSL = useSsl;
		}

		/// <summary>
		/// Creates a socket with a connected client
		/// </summary>
		public WebSocketClient( TcpClient client ) : this()
		{
			// intiailize the client and the network stream
			_client = client;
			_stream = _client.GetStream();

			// presume we're already connected
			this.Connected = true;

			// start receiving data
			this.BeginReceive();
		}

		#region IDisposable Members
		/// <summary>
		/// Disposes the client data
		/// </summary>
		public void Dispose()
		{
			// disconnect if we're still connected
			if ( this.Connected )
				this.Disconnect();
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Connects to the WebSocket's host
		/// </summary>
		public void Connect( string resourcePath )
		{
			if ( string.IsNullOrEmpty( resourcePath ) )
				throw new ArgumentNullException( "resourcePath" );

			// create a new connection to server
			_client = new TcpClient( this.HostName, this.Port );
			_stream = _client.GetStream();

			// store the resource we're connecting to
			var handshake = new WebSocketClientHandshake( this.HostName, this.Port, resourcePath );

			// send the WebSocket handshake to the server
			this.SendHandshake(handshake);
			if ( !this.WasHandshakeSuccessful( handshake ) )
				throw new WebSocketHandshakeException();

			// make the connection as successful
			this.Connected = true;

			// start receiving data
			this.BeginReceive();
		}

		/// <summary>
		/// Disconnects from the server
		/// </summary>
		public void Disconnect()
		{
			if ( this.OnConnectionClosed != null )
				this.OnConnectionClosed( this, EventArgs.Empty );

			if ( _stream != null )
				_stream.Close();

			if ( _client != null )
				_client.Close();
			
			this.Connected = false;
		}

		/// <summary>
		/// Sends data to the server
		/// </summary>
		public void Send( string data )
		{
			if ( !this.Connected )
				throw new InvalidOperationException( "Cannot send data; not connected to the server" );

			byte[] buffer = Encoding.UTF8.GetBytes( data );

			_stream.WriteByte( _messageBeginByte );
			_stream.Write( buffer, 0, buffer.Length );
			_stream.WriteByte( _messageEndByte );
			_stream.Flush();
		}
		#endregion

		#region Public Properties
		/// <summary>
		/// The hostname to connect to
		/// </summary>
		public string HostName { get; private set; }

		/// <summary>
		/// The port to connect on
		/// </summary>
		public int Port { get; private set; }

		/// <summary>
		/// Determines if the client is using SSL to connect
		/// </summary>
		public bool UseSSL { get; private set; }

		/// <summary>
		/// Determines if the client is connected to the server or not
		/// </summary>
		public bool Connected { get; private set; }
		#endregion

		#region Public Events
		/// <summary>
		/// An event that is raised when the client disconnects from the server
		/// </summary>
		public event EventHandler OnConnectionClosed;

		/// <summary>
		/// An event that is raised when a message is received
		/// </summary>
		public event EventHandler<WebSocketEventArgs> OnMessageReceived;
		#endregion

		#region Private Methods
		/// <summary>
		/// Sends the WebSocket handshake
		/// </summary>
		private void SendHandshake(WebSocketClientHandshake handshake)
		{
			// get the handshake bytes
			var handshakeBytes = handshake.ToBytes();

			// and send them down the pipe
			_stream.Write( handshakeBytes, 0, handshakeBytes.Length );
			_stream.Flush();
		}

		/// <summary>
		/// Reads data to determine if the handshake was successful
		/// </summary>
		private bool WasHandshakeSuccessful(WebSocketClientHandshake handshake)
		{
			// construct the actual server handshake from the buffer
			var actualServerHandshakeBytes = new byte[_maxPacketSize];
			var actualServerHandshakeSize = _client.Client.Receive( actualServerHandshakeBytes );
			var actualServerHandshake = WebSocketServerHandshake.Parse( actualServerHandshakeBytes, actualServerHandshakeSize );

			// use the client handshake to generate a valid server handshake for testing
			var clientHandshake = handshake.ToBytes();
			var clientHandshakeSize = clientHandshake.Length;
			var validServerHandshake = WebSocketServerHandshake.FromClientHandshake( clientHandshake, clientHandshakeSize );

			// if they're equal, the handshake was successful
			return validServerHandshake.Equals( actualServerHandshake );
		}
				
		/// <summary>
		/// Starts receiving data on the socket
		/// </summary>
		private void BeginReceive()
		{
			// start an async operation to receive data
			_client.Client.BeginReceive( _buffer, 0, _maxPacketSize, SocketFlags.None, OnDataReceived, null );
		}

		/// <summary>
		/// An event that is raised when data is received by the client socket
		/// </summary>
		private void OnDataReceived( IAsyncResult result )
		{
			if ( !_client.Client.Connected )
				return;

			var bytesRead = _client.Client.EndReceive( result );

			// if no bytes were read, the client was disconnected
			if ( bytesRead == 0 )
			{
				this.Disconnect();
				return;
			}

			// get a copy of the bytes read
			var buffer = _buffer.Take( bytesRead ).ToArray();

			// extract as many messages as possible
			while ( this.ContainsMessage( buffer ) )
				this.RaiseReceivedEvent( this.ExtractMessage( ref buffer ) );

			if ( buffer.Length > 0 )
				throw new Exception( "buffer isn't empty" );

			// start receiving where we left off
			this.BeginReceive();
		}

		/// <summary>
		/// Determines if there is a full message in the data buffer
		/// </summary>
		private bool ContainsMessage( byte[] buffer )
		{
			if ( buffer == null || !buffer.Any() )
				return false;

			var startMessageIndex = Array.IndexOf( buffer, _messageBeginByte );
			var endMessageIndex = Array.IndexOf( buffer, _messageEndByte, startMessageIndex );

			// we found a message in the buffer
			if ( startMessageIndex > -1 && endMessageIndex > startMessageIndex )
				return true;

			// we couldn't find a full message
			return false;
		}

		/// <summary>
		/// Extracts a full message from the buffer
		/// </summary>
		private byte[] ExtractMessage( ref byte[] buffer )
		{
			var startMessageIndex = Array.IndexOf( buffer, _messageBeginByte );
			var endMessageIndex = Array.IndexOf( buffer, _messageEndByte, startMessageIndex ) + 1;

			// extract the message from the buffer
			var message = buffer.Skip( startMessageIndex ).Take( endMessageIndex - startMessageIndex ).ToArray();

			// remove the message from the buffer
			buffer = buffer.Skip( endMessageIndex ).ToArray();

			return message;
		}

		/// <summary>
		/// An event that is called when a message is extracted from the buffer
		/// </summary>
		private void RaiseReceivedEvent( byte[] dataFrameBytes )
		{
			// remove the beginning and ending data frame markers
			var messageBytes = dataFrameBytes.Skip( 1 ).Take( dataFrameBytes.Length - 2 ).ToArray();
			var message = Encoding.UTF8.GetString( messageBytes );

			// for debugging
			System.Diagnostics.Debug.Print( message );

			// raise the event
			if ( this.OnMessageReceived != null )
				this.OnMessageReceived( this, new WebSocketEventArgs( message ) );
		}
		#endregion

		#region Private Members
		/// <summary>
		/// The underlying client object
		/// </summary>
		private TcpClient _client;

		/// <summary>
		/// The network stream for the client connection
		/// </summary>
		private NetworkStream _stream;

		/// <summary>
		/// The buffer for incoming data
		/// </summary>
		private byte[] _buffer;
		#endregion

		#region Constants
		/// <summary>
		/// The maximum packet size that we want to transfer at once (16k)
		/// </summary>
		private const int _maxPacketSize = 16384;

		/// <summary>
		/// The byte used to start a packet
		/// </summary>
		private const byte _messageBeginByte = 0x00;

		/// <summary>
		/// The byte used to end a packet
		/// </summary>
		private const byte _messageEndByte = 0xff;
		#endregion
	}
}
